package nr.co.mhgames.polyanim.renderer;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

import nr.co.mhgames.polyanim.Animation;
import nr.co.mhgames.polyanim.Group;
import nr.co.mhgames.polyanim.KeyFrame;
import nr.co.mhgames.polyanim.Polygon;
import nr.co.mhgames.polyanim.renderer.modifiers.AlphaModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.ColorModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.FlipModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.HSLModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.RenderModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.RotateModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.ScaleModifier;
import nr.co.mhgames.polyanim.renderer.modifiers.FlipModifier.FlipDirection;
import nr.co.mhgames.polyanim.util.ColorDef;
import nr.co.mhgames.polyanim.util.Matrix;
import nr.co.mhgames.polyanim.util.Vector2f;
import nr.co.mhgames.polyanim.util.Vertex;

/**
 * Java 2D renderer for PolyAnim. It uses the plain Java 2D API to render the
 * animations. It's not very accurate since Java 2D polygons are only integer
 * precision, so the animation is not very smooth. We get a little better
 * results by computing the scaling ourselves, instead of letting Graphics2D do
 * it.
 * <p>
 * Java 2D renderer supports concave and self-intersecting polygons, unlike the
 * LWJGL renderer.
 * <p>
 * The renderer can also make use of hardware acceleration, using the
 * (unsupported) acceleration options in JVM.
 * 
 * @author Mika Halttunen
 */
public class Java2DAnimRenderer extends AnimRenderer {
	/** Singleton instance */
	private static Java2DAnimRenderer singleton;

	/** 1 pixel thin stroke for the outlines */
	private static BasicStroke blackStroke = new BasicStroke(0);

	/** Possible (unsupported) hardware accelerations */
	public enum HWAcceleration {
		NONE, D3D, OPENGL
	}

	/** Acceleration used */
	private HWAcceleration accel = HWAcceleration.NONE;

	/** Graphics context */
	private Graphics2D g2d = null;

	/** Scale factor, set per polygon basis prior rendering a poly */
	private Vector2f scaleFactor;


	/**
	 * Private constructor, you must use {@link #getInstance()} instead.
	 */
	private Java2DAnimRenderer() {
		try {
			scaleFactor = (Vector2f) Vector2f.UNIT.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Return the Java 2D renderer instance.
	 * 
	 * @return Java 2D renderer
	 */
	public static AnimRenderer getInstance() {
		if (singleton == null) {
			singleton = new Java2DAnimRenderer();
		}

		return singleton;
	}

	public String getIdentifier() {
		return "Java 2D";
	}

	/**
	 * Set the graphics context used for rendering. This should be called in a
	 * paint()-method.
	 * 
	 * @param graphics
	 *            Graphics context
	 */
	public void setContext(Graphics2D graphics) {
		g2d = graphics;
	}


	/**
	 * Suggest JVM to try using hardware acceleration. On Windows systems
	 * Direct3D acceleration should work nicely. The performance gains through
	 * OpenGL acceleration might vary. On Linux systems OpenGL is the only
	 * option.
	 * <p>
	 * Note that these options are unsupported, at least before Java 6u10, and
	 * may not work. This method should be called before creating any
	 * windows/frames.
	 * 
	 * @param accel
	 *            Desired acceleration
	 */
	public void setHWAcceleration(HWAcceleration accel) {
		this.accel = accel;
		switch (accel) {
		default:
		case NONE:
			System.setProperty("sun.java2d.opengl", "false");
			System.setProperty("sun.java2d.d3d", "false");
			break;
		case OPENGL:
			System.setProperty("sun.java2d.opengl", "True");
			System.setProperty("sun.java2d.d3d", "false");
			break;
		case D3D:
			System.setProperty("sun.java2d.opengl", "false");
			System.setProperty("sun.java2d.d3d", "True");
			break;
		}
	}

	/**
	 * Return the current HW acceleration method, if any.
	 * 
	 * @return HW acceleration method used
	 */
	public HWAcceleration getHWAcceleration() {
		return accel;
	}

	public void renderPolygon(Polygon poly) {
		assert (g2d != null);

		// Set the color (multiply with the alpha factor)
		ColorDef color = poly.getColor();

		// Use the precreated ColorDef instance, to avoid garbage
		tmpColor.set(color.red, color.green, color.blue,
				(int) (color.alpha * ActiveModifiers.alphaFactor));
		// Modulate the color
		if (ActiveModifiers.activeHSL != null) {
			ActiveModifiers.activeHSL.modulate(tmpColor);
		}

		// Coloring?
		if (ActiveModifiers.activeColoring != null)
			ActiveModifiers.activeColoring.transformColor(tmpColor);

		final Color paint = new Color(tmpColor.red, tmpColor.green, tmpColor.blue, tmpColor.alpha);
		g2d.setPaint(paint);

		// Retrieve the points
		final int xpoints[] = new int[poly.getVertexCount()];
		final int ypoints[] = new int[poly.getVertexCount()];
		int i = 0;
		for (Vertex v: poly.getVertices()) {
			xpoints[i] = (int) (v.position.x * scaleFactor.x);
			ypoints[i] = (int) (v.position.y * scaleFactor.y);
			++i;
		}

		// Draw the polygon
		g2d.fillPolygon(xpoints, ypoints, poly.getVertexCount());

		// Draw the outlines
		if (shouldDrawOutlines()) {
			g2d.setColor(Color.black);
			g2d.setStroke(blackStroke);
			g2d.drawPolygon(xpoints, ypoints, poly.getVertexCount());
		}
	}

	public void renderGroup(Group group) {
		assert (g2d != null);
		// Store the current transform
		AffineTransform oTransform = g2d.getTransform();

		// Translate to the offset
		Vector2f offset = group.getOffset();
		g2d.translate(offset.x, offset.y);


		// Rotate around the pivot point
		if (group.getRotation() != 0.0f) {
			Vector2f pivot = group.getPivot();
			double rad = group.getRotation() * Math.PI / 180.0;
			g2d.rotate(rad, pivot.x, pivot.y);
		}

		// Scale
		scaleFactor = group.getScale();

		// We can get a bit better results by computing the scaling ourselves in
		// renderPolygon(), instead of letting g2d do it.

		// g2d.scale(scale.x, scale.y);

		// Draw the group polygons
		for (Polygon poly: group.getPolygons())
			renderPolygon(poly);

		// Restore the original transform
		g2d.setTransform(oTransform);


		// TODO: Remove this test
		// Matrix tr = group.computeTransforms();
		//
		// g2d.setColor(Color.white);
		// Vector2f point = tr
		// .transformPoint(group.getPolygon(0).getVertex(0).position);
		// Vector2f point2 = tr
		// .transformPoint(group.getPolygon(0).getVertex(1).position);
		// g2d.drawLine((int) point.x, (int) point.y, (int) point2.x,
		// (int) point2.y);
		//
		// applyMatrix(tr);
		//
		// Polygon poly = group.getPolygon(0);
		// Color c = poly.getColor();
		// poly.setColor(new Color(255, 255, 255, 128));
		// renderPolygon(poly);
		// poly.setColor(c);
		// g2d.setTransform(oTransform);
	}

	public void renderKeyFrame(KeyFrame frame) {
		// We need to make a copy of the group map values as not to disturb the
		// original order.
		HashMap<String, Group> groupMap = frame.getGroups();
		ArrayList<Group> groups = new ArrayList<Group>(groupMap.values());

		// Sort by the depth value
		Collections.sort(groups);

		// Draw the polygon groups
		for (Group g: groups) {
			if (g.isVisible())
				renderGroup(g);
		}

		// Restore the scale factor
		scaleFactor = Vector2f.UNIT;
	}

	public void renderAnimation(Animation anim) {
		renderKeyFrame(anim.getCurrentFrame());
	}

	public void renderAnimation(Animation anim, float offsetX, float offsetY) {
		// Store the current transform
		AffineTransform oTransform = g2d.getTransform();

		// Translate to the offset and render
		g2d.translate(offsetX, offsetY);
		renderAnimation(anim);

		// Restore the original transform
		g2d.setTransform(oTransform);
	}


	@Override
	public void renderAnimation(Animation anim, float offsetX, float offsetY,
			RenderModifier... modifiers) {
		// Store the current transform
		AffineTransform oTransform = g2d.getTransform();

		// Translate to the offset and render
		g2d.translate(offsetX, offsetY);

		// Store the HSL and color modifiers
		storeRenderModifiers();

		// Apply the modifiers
		for (RenderModifier mod: modifiers)
			applyRenderModifier(mod);

		// Render
		renderAnimation(anim);

		// Restore the original transform
		g2d.setTransform(oTransform);

		// Restore the HSL and color modifiers
		restoreRenderModifiers();
	}

	/**
	 * Applies the given render modifier for the next rendering call.
	 * 
	 * @param mod
	 *            Render modifier
	 */
	private void applyRenderModifier(RenderModifier mod) {
		// Flips
		if (FlipModifier.class.isInstance(mod)) {
			FlipModifier m = (FlipModifier) mod;
			if (m.direction == FlipDirection.VERTICAL)
				g2d.scale(1, -1);
			else if (m.direction == FlipDirection.HORIZONTAL)
				g2d.scale(-1, 1);
			else
				g2d.scale(-1, -1);
		}
		// Rotations
		else if (RotateModifier.class.isInstance(mod)) {
			RotateModifier m = (RotateModifier) mod;
			g2d.rotate(m.rotation * Math.PI / 180.0);
		}
		// Scale
		else if (ScaleModifier.class.isInstance(mod)) {
			ScaleModifier m = (ScaleModifier) mod;
			g2d.scale(m.scaleX, m.scaleY);
		}
		// HSL
		else if (HSLModifier.class.isInstance(mod)) {
			ActiveModifiers.activeHSL = (HSLModifier) mod;
		}
		// Coloring
		else if (ColorModifier.class.isInstance(mod)) {
			ActiveModifiers.activeColoring = (ColorModifier) mod;
		}
		// Alpha modifier
		else if (AlphaModifier.class.isInstance(mod)) {
			ActiveModifiers.alphaFactor = ((AlphaModifier) mod).factor;
		}
	}


	/**
	 * Multiply the current drawing transform by the given matrix.
	 * 
	 * @param mat
	 *            Matrix to multiply with
	 */
	public void applyMatrix(Matrix mat) {
		assert (g2d != null);
		float m[][] = new float[4][4];
		mat.getM(m);

		// Java 2D uses 3x3 matrices instead of 4x4
		AffineTransform tx = new AffineTransform(m[0][0], m[0][1], m[1][0], m[1][1], m[3][0],
				m[3][1]);
		g2d.transform(tx);
	}
}
